package com.theksmith.android.car_bus_interface;

import android.app.Activity;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.graphics.Color;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.Messenger;
import android.os.RemoteException;
import android.text.Html;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.style.ForegroundColorSpan;
import android.util.Log;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.inputmethod.EditorInfo;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ScrollView;
import android.widget.TextView;

import com.theksmith.android.helpers.AppState;

import java.text.SimpleDateFormat;
import java.util.Calendar;

import static com.theksmith.android.car_bus_interface.BusData.*;


/*
todo: refactor to not use the service binding for monitoring bus data...

there are 2 main problems with the current approach:

 -  the terminal only logs when the activity has focus

 -  large/fast data hangs the UI, such as after issuing the ATMA command on a busy bus (the binding is flooded with RX bus data messages)

possible solutions, instead of sending bus data across the binding:

 -  have the service log all bus data to a Shared Preferences item
    this likely requires a serialized array or collection of some type and all the resultant serialize/de-serialize calls would probably be slow and memory intensive
    could also run into concurrent access issues with the state item

 -  use Internal Storage or External Storage providers to log all bus data to a file
    should be fast and easy to write to (append), unsure how easy to reading only the tail is?
    need to consider whether to use External which will require permissions and more writable/readable checks but allow user to grab the file
    or for simplicity use Internal since this is supposed to be a SIMPLE debugging feature and not a full app in itself (could always add ability to export the file)
    if we don't do External, then maybe just use app cache storage for the file (see getCacheDir()), then it gets cleaned up automatically on app uninstall

*/

/**
 * a debugging terminal screen Activity
 * binds to CBIServiceMain to allow monitoring bus data and sending bus commands
 * this is launched from within the Settings screen
 *
 * @author Kristoffer Smith <[email protected]>
 */
public class CBIActivityTerminal extends Activity {
    private static final String TAG = "CBIActivityTerminal";
    private static final boolean D = BuildConfig.SHOW_DEBUG_LOG_LEVEL > 0;
    private static final boolean DD = BuildConfig.SHOW_DEBUG_LOG_LEVEL > 1;

    private MenuItem mMenuItemScroll;
    private MenuItem mMenuItemShowTime;
    private MenuItem mMenuItemShowElapsed;

    private TextView mTxtTerminal;
    private EditText mTxtCommand;
    private TextView mTxtAlertOverlay;

    private boolean mAutoScroll;
    private boolean mShowTime;
    private boolean mShowElapsed;

    private boolean mIsBound;

    private Messenger mServiceMainMessenger = null;
    private final Messenger mServiceMainIncomingMessenger = new Messenger(new ServiceMainHandler());

    private long mLastTerminalTime;

    private static final int MAX_TERMINAL_LINES = 550;
    private static final int MAX_TERMINAL_LINES_TRIM = 50; //how much space to make each time we reach MAX_TERMINAL_LINES (trimming a single line at a time would result in constant UI updates)


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        if (D) Log.d(TAG, "onCreate()");

        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_terminal);

        mTxtTerminal = (TextView) findViewById(R.id.txtTerminal);
        mTxtTerminal.setOnTouchListener(mTxtTerminal_OnTouchListener);

        mTxtCommand = (EditText) findViewById(R.id.txtCommand);
        mTxtCommand.setOnEditorActionListener(mTxtCommand_OnEditorActionListener);

        mTxtAlertOverlay = (TextView) findViewById(R.id.txtAlertOverlay);

        final Button btnSend = (Button) findViewById(R.id.btnSend);
        btnSend.setOnClickListener(mBtnSend_OnClickListener);
    }

    @Override
    protected void onStart() {
        if (D) Log.d(TAG, "onStart()");

        super.onStart();

        mTxtTerminal.setText("");

        serviceMainBind();
    }


    @Override
    protected void onDestroy() {
        if (D) Log.d(TAG, "onDestroy()");

        super.onDestroy();

        serviceMainUnBind();
    }

    @Override
    protected void onResume() {
        if (D) Log.d(TAG, "onResume()");

        super.onResume();

        //persist terminal contents
        String html = AppState.getString(getApplicationContext(), R.string.app_state_s_termninal_contents, "");
        mTxtTerminal.setText(Html.fromHtml(html));

        //alert users that terminal is once again active
        BusData data = new BusData(getString(R.string.msg_error_terminal_focus_resumed), BusDataType.ERROR, false);
        terminalAppend(data);
    }

    @Override
    protected void onPause() {
        if (D) Log.d(TAG, "onPause()");

        super.onPause();

        //alert users that terminal was not logging while out of focus
        BusData data = new BusData(getString(R.string.msg_error_terminal_focus_lost), BusDataType.ERROR, false);
        terminalAppend(data);

        //persist terminal contents
        SpannableString span = SpannableString.valueOf(mTxtTerminal.getText());
        AppState.setString(getApplicationContext(), R.string.app_state_s_termninal_contents, Html.toHtml(span));
    }

    @Override
    public boolean onCreateOptionsMenu(final Menu menu) {
        if (D) Log.d(TAG, "onCreateOptionsMenu()");

        getMenuInflater().inflate(R.menu.actvity_terminal, menu);

        mMenuItemScroll = menu.findItem(R.id.action_scroll);
        toggleAutoScroll(mMenuItemScroll.isChecked());

        mMenuItemShowTime = menu.findItem(R.id.action_show_time);
        toggleShowTime(mMenuItemShowTime.isChecked());

        mMenuItemShowElapsed = menu.findItem(R.id.action_show_elapsed);
        toggleShowElapsed(mMenuItemShowElapsed.isChecked());

        return true;
    }

    @Override
    public boolean onOptionsItemSelected(final MenuItem item) {
        if (D) Log.d(TAG, "onOptionsItemSelected()");

        final int id = item.getItemId();

        if (id == R.id.action_scroll) {
            toggleAutoScroll(!item.isChecked());
            return true;
        } else if (id == R.id.action_show_time) {
            toggleShowTime(!item.isChecked());
            return true;
        } else if (id == R.id.action_show_elapsed) {
            toggleShowElapsed(!item.isChecked());
            return true;
        } else if (id == R.id.action_send_startup) {
            mTxtCommand.setText("");
            serviceMainSendStartupCommands();
            toggleAutoScroll(true);
            return true;
        }

        return super.onOptionsItemSelected(item);
    }

    private View.OnClickListener mBtnSend_OnClickListener = new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            if (D) Log.d(TAG, "mBtnSend_OnClickListener.onClick()");

            sendCommand();
        }
    };

    private EditText.OnEditorActionListener mTxtCommand_OnEditorActionListener = new TextView.OnEditorActionListener() {
        @Override
        public boolean onEditorAction(final TextView textView, final int actionId, final KeyEvent keyEvent) {
            if (D) Log.d(TAG, "mTxtCommand_OnEditorActionListener.onEditorAction()");

            //handle the the virtual keyboard send button or physical keyboard action/enter button
            if (actionId == EditorInfo.IME_ACTION_SEND || (actionId == EditorInfo.IME_NULL && keyEvent.getAction() == KeyEvent.ACTION_DOWN)) {
                sendCommand();
                return true;
            }

            return false;
        }
    };

    final TextView.OnTouchListener mTxtTerminal_OnTouchListener = new View.OnTouchListener() {
        @Override
        public boolean onTouch(View view, MotionEvent motionEvent) {
            if (DD) Log.d(TAG, "mTxtTerminal_OnTouchListener.onTouch()");

            toggleAutoScroll(false);
            return true;
        }
    };

    private void toggleAutoScroll(final boolean scroll) {
        if (D) Log.d(TAG, "toggleAutoScroll() : scroll= " + scroll);

        mAutoScroll = scroll;
        mMenuItemScroll.setChecked(mAutoScroll);

        if (mAutoScroll) {
            mTxtAlertOverlay.setVisibility(View.GONE);
            terminalScroll();
        } else {
            mTxtAlertOverlay.setText(getString(R.string.msg_alert_scroll_off));
            mTxtAlertOverlay.setVisibility(View.VISIBLE);
        }
    }

    private void toggleShowTime(final boolean show) {
        if (D) Log.d(TAG, "toggleShowTime() : show= " + show);

        mShowTime = show;
        mMenuItemShowTime.setChecked(mShowTime);
    }

    private void toggleShowElapsed(final boolean show) {
        if (D) Log.d(TAG, "toggleShowElapsed() : show= " + show);

        mShowElapsed = show;
        mMenuItemShowElapsed.setChecked(mShowElapsed);
    }

    private void terminalScroll() {
        if (DD) Log.d(TAG, "terminalScroll()");

        final ScrollView scrollView = (ScrollView) findViewById(R.id.scrollView);

        scrollView.post(new Runnable()
        {
            public void run()
            {
                scrollView.smoothScrollTo(0, mTxtTerminal.getBottom());
            }
        });
    }

    private void terminalAppend(final BusData data) {
        if (DD) Log.d(TAG, "terminalAppend()");

        //keep a rolling terminal of MAX_TERMINAL_LINES number of lines
        final int lines = mTxtTerminal.getLineCount();
        if (lines > MAX_TERMINAL_LINES) {
            final StringBuilder trimmed = new StringBuilder();
            final String html = Html.toHtml(SpannableString.valueOf(mTxtTerminal.getText()));

            final TextUtils.SimpleStringSplitter splitter = new TextUtils.SimpleStringSplitter('\n');
            splitter.setString(html);

            int l = 0;
            for (final String line : splitter) {
                l++;
                if (l > (lines - MAX_TERMINAL_LINES + MAX_TERMINAL_LINES_TRIM)) {
                    trimmed.append(line);
                }
            }

            mTxtTerminal.setText(Html.fromHtml(trimmed.toString()));
        }

        //color code the data
        ForegroundColorSpan color = new ForegroundColorSpan(Color.GRAY);

        if (data.type == BusDataType.RX_MONITORED) {
            color = new ForegroundColorSpan(Color.GREEN);
        } else if (data.type == BusDataType.TX) {
            color = new ForegroundColorSpan(Color.WHITE);
        } else if (data.type == BusDataType.ERROR) {
            color = new ForegroundColorSpan(Color.RED);
        }

        //do timestamp and elapsed span stuff
        final Calendar now = Calendar.getInstance();

        String nowStamp = "";
        if (mShowTime) {
            final SimpleDateFormat formatter = new SimpleDateFormat("hh:mm:ss.SSS");
            nowStamp = formatter.format(now.getTime()) + " ";
        }

        String diffStamp = "";
        if (mShowElapsed) {
            final Calendar diff = Calendar.getInstance();
            if (mLastTerminalTime <= 0) {
                mLastTerminalTime = now.getTimeInMillis();
            }
            final long diffMillis = now.getTimeInMillis() - mLastTerminalTime;
            diff.setTimeInMillis(diffMillis);
            final SimpleDateFormat formatter = new SimpleDateFormat("mm:ss.SSS");
            diffStamp = "(" + formatter.format(diff.getTime()) + ") ";
        }

        mLastTerminalTime = now.getTimeInMillis();

        //assemble the final line and add to the terminal
        String message = nowStamp + diffStamp + data.data + "\n";
        SpannableString span = new SpannableString(message);
        span.setSpan(color, 0, message.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
        mTxtTerminal.append(span);

        //if RX is complete, show a prompt marker
        if (data.rxComplete) {
            message = ">";
            span = new SpannableString(message);
            span.setSpan(Color.WHITE, 0, message.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
            mTxtTerminal.append(span);
        }

        if (mAutoScroll) {
            terminalScroll();
        }
    }

    private void sendCommand() {
        if (D) Log.d(TAG, "sendCommand()");

        serviceMainSendBusCommand(mTxtCommand.getText().toString());

        mTxtCommand.setText("");

        toggleAutoScroll(true);
    }

    void serviceMainLostBinding() {
        if (D) Log.d(TAG, "serviceMainLostBinding()");

        BusData data = new BusData(getString(R.string.msg_error_terminal_lost_binding), BusDataType.ERROR, false);
        terminalAppend(data);

        toggleAutoScroll(true);
    }

    void serviceMainBind() {
        if (D) Log.d(TAG, "serviceMainBind()");

        bindService(new Intent(CBIActivityTerminal.this, CBIServiceMain.class), mServiceMainConnection, Context.BIND_AUTO_CREATE);
        mIsBound = true;
    }

    void serviceMainUnBind() {
        if (D) Log.d(TAG, "serviceMainUnBind()");

        if (mIsBound) {
            if (mServiceMainMessenger != null) {
                try {
                    Message message = Message.obtain(null, CBIServiceMain.BOUND_MSG_UNREGISTER_CLIENT);
                    message.replyTo = mServiceMainIncomingMessenger;
                    mServiceMainMessenger.send(message);
                } catch (RemoteException ignored) {}
            }

            unbindService(mServiceMainConnection);
            mIsBound = false;
        }
    }

    private final ServiceConnection mServiceMainConnection = new ServiceConnection() {
        public void onServiceConnected(final ComponentName className, final IBinder service) {
            if (D) Log.d(TAG, "mServiceMainConnection : onServiceConnected()");

            mServiceMainMessenger = new Messenger(service);

            try {
                Message message = Message.obtain(null, CBIServiceMain.BOUND_MSG_REGISTER_CLIENT);
                message.replyTo = mServiceMainIncomingMessenger;
                mServiceMainMessenger.send(message);
            } catch (RemoteException e) {
                serviceMainLostBinding();
            }
        }

        public void onServiceDisconnected(final ComponentName className) {
            if (D) Log.d(TAG, "mServiceMainConnection : onServiceDisconnected()");

            mServiceMainMessenger = null;
        }
    };

    class ServiceMainHandler extends Handler {
        @Override
        public void handleMessage(final Message message) {
            if (DD) Log.d(TAG, "ServiceMainHandler : handleMessage()");

            switch (message.what) {
                case CBIServiceMain.BOUND_MSG_NOTIFY_BUS_DATA:
                    if (message.obj != null) {
                        BusData data = (BusData) message.obj;
                        terminalAppend(data);
                    }
                    break;

                default:
                    super.handleMessage(message);
            }
        }
    }

    private void serviceMainSendBusCommand(final String command) {
        if (D) Log.d(TAG, "serviceMainSendBusCommand()");

        try {
            Message message = Message.obtain(null, CBIServiceMain.BOUND_MSG_SEND_BUS_COMMAND, command);
            message.replyTo = mServiceMainIncomingMessenger;
            mServiceMainMessenger.send(message);
        } catch (RemoteException e) {
            serviceMainLostBinding();
        }
    }

    private void serviceMainSendStartupCommands() {
        if (D) Log.d(TAG, "serviceMainSendStartupCommands()");

        try {
            Message message = Message.obtain(null, CBIServiceMain.BOUND_MSG_SEND_STARTUP_COMMANDS);
            message.replyTo = mServiceMainIncomingMessenger;
            mServiceMainMessenger.send(message);
        } catch (RemoteException e) {
            serviceMainLostBinding();
        }
    }
}